﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Input;
using Windows.Foundation;
using Windows.Storage;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Input;
using Windows.UI.Input.Inking;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace App89
{
    public class InkCanvas : Canvas
    {
        // constructor
        public InkCanvas()
        {
            // defaults
            VerticalAlignment = Windows.UI.Xaml.VerticalAlignment.Stretch;
            HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Stretch;
            Background = new SolidColorBrush(Colors.White);
            InkMode = InkManipulationMode.Inking;
            PenColor = Colors.Black;
            PenSize = 2d;

            // events
            this.PointerPressed += (s, e) =>
            {
                // test input
                var point = e.GetCurrentPoint(this);
                if (!point.Properties.IsLeftButtonPressed)
                    return;
                switch (point.PointerDevice.PointerDeviceType)
                {
                    case PointerDeviceType.Mouse:
                    case PointerDeviceType.Pen:
                        _PenId = point.PointerId;
                        break;
                    case PointerDeviceType.Touch:
                        break;
                }

                // handle
                HandlePressed(point);
                e.Handled = true;
            };

            this.PointerMoved += (s, e) =>
            {
                // test input
                var point = e.GetCurrentPoint(this);
                if (!point.Properties.IsLeftButtonPressed)
                    return;
                switch (point.PointerDevice.PointerDeviceType)
                {
                    case PointerDeviceType.Mouse:
                    case PointerDeviceType.Pen:
                        if (point.PointerId != _PenId)
                            return;
                        break;
                    case PointerDeviceType.Touch:
                        break;
                }

                // handle
                HandleMoved(point);
                e.Handled = true;
            };

            PointerEventHandler releasedHandler = (s, e) =>
            {
                // test input
                var point = e.GetCurrentPoint(this);
                switch (point.PointerDevice.PointerDeviceType)
                {
                    case PointerDeviceType.Mouse:
                    case PointerDeviceType.Pen:
                        if (point.PointerId != _PenId)
                            return;
                        _PenId = default(uint);
                        break;
                    case PointerDeviceType.Touch:
                        break;
                }

                // handle
                HandleReleased(point);
                e.Handled = true;
            };
            this.PointerReleased += releasedHandler;
            this.PointerExited += releasedHandler;

            // setup RealTime visual
            _RealTimeVisual = new Path
            {
                Stroke = new SolidColorBrush(PenColor),
                StrokeThickness = PenSize,
                Data = new PathGeometry { Figures = { new PathFigure { Segments = { new PolyLineSegment() }, }, } },
            };
            _RealTimeVisual.SetValue(Canvas.ZIndexProperty, 1000);
            this.Children.Add(_RealTimeVisual);

            // cursor
            var originalCursor = Window.Current.CoreWindow.PointerCursor;
            PointerEntered += (s, args) =>
            {
                if (InkMode == InkManipulationMode.Erasing)
                    Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Hand, 0);
                else
                    Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Cross, 0);
            };
            PointerExited += (s, args) => { Window.Current.CoreWindow.PointerCursor = originalCursor; };
        }

        // the line the user is currently drawing
        private Path _RealTimeVisual;

        // unique id depending on input type
        private uint _PenId = default(uint);

        // recognizer
        private InkManager _Manager = new InkManager();

        // write or erase
        public InkManipulationMode InkMode
        {
            get { return _Manager.Mode; }
            set { _Manager.Mode = value; }
        }

        // size of line
        public double PenSize { get; set; }

        // color of line
        public Color PenColor { get; set; }

        // get available languages
        public string[] GetLanguages() { return _Manager.GetRecognizers().Select(x => x.Name).ToArray(); }

        InkRecognizer _CustomRecognizer;
        // set custom langauge
        public void SetLanguage(string name)
        {
            var match = _Manager.GetRecognizers().FirstOrDefault(x => x.Name.Equals(name));
            if (match == null)
                throw new ArgumentOutOfRangeException("name not found, use GetLanguages()");
            _CustomRecognizer = match;
        }

        // handle pointer down
        private void HandlePressed(PointerPoint point)
        {
            // start ink
            var attributes = new InkDrawingAttributes
            {
                Size = new Size(PenSize, PenSize),
                Color = PenColor,
                FitToCurve = true,
                PenTip = PenTipShape.Circle,
            };
            _Manager.SetDefaultDrawingAttributes(attributes);
            _Manager.ProcessPointerDown(point);

            // start RealTime
            switch (InkMode)
            {
                case InkManipulationMode.Inking:
                    // setup tempoarary path
                    _RealTimeVisual.Stroke = new SolidColorBrush(PenColor);
                    _RealTimeVisual.StrokeThickness = PenSize;
                    var figure = (_RealTimeVisual.Data as PathGeometry).Figures.First() as PathFigure;
                    figure.StartPoint = point.Position;
                    var segment = (figure.Segments.First() as PolyLineSegment);
                    segment.Points.Clear();
                    break;
                case InkManipulationMode.Erasing:
                    // nothing to do
                    break;
            }
        }

        int _PreviousStrokeCount = 0;
        // handle pointer moved
        private void HandleMoved(PointerPoint point)
        {
            // update ink
            _Manager.ProcessPointerUpdate(point);

            // update RealTime
            switch (InkMode)
            {
                case InkManipulationMode.Inking:

                    // RealTime visual
                    var current = point.Position;
                    var figure = (_RealTimeVisual.Data as PathGeometry).Figures.First() as PathFigure;
                    var segment = (figure.Segments.First() as PolyLineSegment);
                    segment.Points.Add(current);
                    break;

                case InkManipulationMode.Erasing:

                    // refresh to show missing
                    var strokeCount = _Manager.GetStrokes().Count;

                    // but, only if a refresh is necessary
                    if (strokeCount != _PreviousStrokeCount)
                    {
                        _PreviousStrokeCount = strokeCount;
                        RedrawCanvas();

                        // bubble change
                        RaiseLineRemoved();
                    }
                    break;
            }
        }

        // handle pointer released
        private void HandleReleased(PointerPoint point)
        {
            // end ink
            _Manager.ProcessPointerUp(point);

            // end RealTime
            RedrawCanvas();

            // bubble change
            if (InkMode == InkManipulationMode.Inking)
                RaiseLineAdded();
        }

        // clear canvas and update with strokes, again
        private void RedrawCanvas()
        {
            // clear temp visual 
            var figure = (_RealTimeVisual.Data as PathGeometry).Figures.First() as PathFigure;
            figure.StartPoint = default(Point);
            var segment = (figure.Segments.First() as PolyLineSegment);
            segment.Points.Clear();

            // remove all other strokes
            var paths = this.Children.OfType<Path>().Where(x => x.Tag is InkStroke);
            foreach (var item in paths)
                this.Children.Remove(item);

            // add stokes to canvas
            foreach (var path in _Manager.ToPath())
                this.Children.Add(path);
        }

        // clear canvas, reset manager
        public async Task Clear()
        {
            this.Children.Clear();
            _Manager = new InkManager();
        }

        // attempt to recognize handwriting
        public async Task<IReadOnlyList<InkRecognitionResult>> RecognizeHandwriting()
        {
            var recognizers = _Manager.GetRecognizers();
            // use user-selected or first as default
            var inkRecognizer = _CustomRecognizer ?? recognizers.First();
            _Manager.SetDefaultRecognizer(inkRecognizer);
            return await _Manager.RecognizeAsync(InkRecognitionTarget.All);
        }

        // utility method, unsnap for dialogs
        private bool EnsureUnSnap() { return ((ApplicationView.Value != ApplicationViewState.Snapped) || ApplicationView.TryUnsnap()); }

        // save to file
        public async Task Save(string suggestedFileName = "ink.png")
        {
            if (!EnsureUnSnap())
                return;

            // prompt for location
            var picker = new Windows.Storage.Pickers.FileSavePicker
            {
                SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Desktop,
                DefaultFileExtension = ".png",
            };
            picker.FileTypeChoices.Add("PNG", new string[] { ".png" });
            picker.SuggestedFileName = suggestedFileName;
            var target = await picker.PickSaveFileAsync();
            if (target == null)
                return;

            // write ink
            using (var stream = await target.OpenAsync(FileAccessMode.ReadWrite))
            {
                if (stream != null)
                    await _Manager.SaveAsync(stream);
            }
        }

        // load from file
        public async Task Load()
        {
            if (!EnsureUnSnap())
                return;

            // prompt for file
            var picker = new Windows.Storage.Pickers.FileOpenPicker
            {
                SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Desktop,
            };
            picker.FileTypeFilter.Add(".png");
            var source = await picker.PickSingleFileAsync();
            if (source != null)
                await Load(source);
        }

        // load from file (specified)
        public async Task Load(StorageFile source)
        {
            if (source == null)
                throw new ArgumentNullException("source");

            // read ink
            using (var stream = await source.OpenAsync(Windows.Storage.FileAccessMode.Read))
            {
                await _Manager.LoadAsync(stream);
                RedrawCanvas();
            }
        }

        #region LineRemoved event

        public class LineRemovedEventArgs : System.EventArgs { }
        public event EventHandler<LineRemovedEventArgs> LineRemoved;
        protected void RaiseLineRemoved()
        {
            if (LineRemoved != null)
                LineRemoved(this, new LineRemovedEventArgs { });
        }

        #endregion

        #region LineAdded event

        public class LineAddedEventArgs : System.EventArgs { }
        public event EventHandler<LineAddedEventArgs> LineAdded;
        protected void RaiseLineAdded()
        {
            if (LineAdded != null)
                LineAdded(this, new LineAddedEventArgs { });
        }

        #endregion
    }

    public static class Extensions
    {
        // convert results to text
        public static String[] ToText(this IReadOnlyList<InkRecognitionResult> results)
        {
            var previousX = 0d;
            var previousY = 0d;
            var firstWord = true;
            var list = new List<string>();

            foreach (var result in results)
            {
                var isNewWord = (Math.Abs(result.BoundingRect.Left - previousX) > 10);
                var isNewLine = (Math.Abs(result.BoundingRect.Bottom - previousY) > 20);

                var one = isNewLine && !firstWord ? "\r\n" : string.Empty;
                var two = isNewWord && !firstWord ? " " : string.Empty;
                var three = result.GetTextCandidates().First();
                list.Add(string.Concat(one, two, three));

                previousX = result.BoundingRect.Right;
                previousY = result.BoundingRect.Bottom;
                firstWord = false;
            }

            return list.ToArray();
        }

        // convert manager's strokes to path
        public static Path[] ToPath(this InkManager manager)
        {
            var paths = new List<Path>();
            foreach (var stroke in manager.GetStrokes())
            {
                // get strokes
                var strokes = stroke.GetRenderingSegments();
                if (!strokes.Any())
                    continue;

                // convert segments
                var segments = strokes.Select(x => new BezierSegment
                {
                    Point1 = x.BezierControlPoint1,
                    Point2 = x.BezierControlPoint2,
                    Point3 = x.Position,
                });
                var collection = new PathSegmentCollection();
                foreach (var segment in segments)
                    collection.Add(segment);

                // build path
                var path = new Path
                {
                    Tag = stroke,
                    Stroke = new SolidColorBrush(stroke.DrawingAttributes.Color),
                    StrokeThickness = stroke.DrawingAttributes.Size.Width,
                    Data = new PathGeometry
                    {
                        Figures = 
                        { 
                            new PathFigure 
                            { 
                                StartPoint = strokes.First().Position, 
                                Segments = collection,
                            }, 
                        }
                    },
                };
                paths.Add(path);
            }
            return paths.ToArray();
        }
    }
}
